package model;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import controller.Output;

/**
 * Das ChatModel h&auml;elt alle wichtigen Informationen um den Chat
 * darzustellen. Dazu z&aumlhlen unter Anderem Chatverlauf, Inhalt der
 * Eingabezeile und Style-Informationen
 * 
 * @author Simon Wirth
 * 
 */
public class ChatModel implements Output{
	/** Repr&auml;sentiert den Chatverlauf */
	private List<HistoryEintrag> nachrichten;
	/** Repr&auml;sentiert den aktuellen Text in der Eingabezeile */
	private List<Character> textZeile;
	/** Index fuer die oberste anzuzeigende Nachricht */
	private int startIndex;
	/** Anzahl an Zeilen, die im Chatfenster angezeigt werden */
	private int zeilenAnzahl;
	/** Anzahl an Zeilen der ganzen History*/
	private int alleZeilen;
	/** Index fuer die Position des Scrollbalkens */
	private int scrollBalkenIndex;
	/** Anzahl wie viele Scrollvor&auml;nge von der obersten zur untersten Zeile noetig sind*/
	private int scrollEinheiten;
	/** Index fuer das erste anzuzeigende Zeichen der Eingabezeile */
	private int textZeilenIndex;
	
	private List<Output> outputs;
	/**
	 * Index fuer die Cursor-Position analog zum textZeilenIndex
	 * 
	 * @see #textZeilenIndex
	 */
	private int cursorIndex;
	/** Der TextCursor */
	private Line2D cursor;
	/** Ob der TextCursor bei naechsten repaint gezeichnet werden soll */
	private boolean zeichneCursor = true;

	// TODO Schriftart, Farben etc.
	/**
	 * Verwendete Schriftarten.
	 */
	private Font plainFont, boldFont, italicFont, editorFont;
	/**
	 * Verwendete Farben
	 */
	private Color hintergrundFarbe, rahmenFarbe, scrollBalkenFarbe, historyTextFarbe, editorTextFarbe, textCursorFarbe;

	/**
	 * DoubleBuffer image fuer schnelleren Repaint.
	 */
	private BufferedImage cache;
	/**
	 * Ob der Cache beim zeichnen erneuert werden muss oder nicht.
	 */
	private boolean repaint = true;

	/**
	 * Initialisiert das ChatModel
	 */
	public ChatModel() {

		nachrichten = new ArrayList<HistoryEintrag>(10);
		textZeile = new LinkedList<Character>();
		outputs = new ArrayList<Output>();
		
		startIndex = 0;
		zeilenAnzahl = 10;
		cursorIndex = 0;
		alleZeilen = 0;
		scrollBalkenIndex = 0;
		scrollEinheiten = 0;
		// textZeilenIndex = 0;
		cursor = new Line2D.Double(0, 0, 0, 0);

		fontVoreinstellungenVornehmen();
		//TODO farbauswahl nict statisch bzw setter  implementieren
		editorTextFarbe = Color.BLACK;
		historyTextFarbe = Color.BLACK;
		hintergrundFarbe = Color.WHITE;
		rahmenFarbe = Color.BLACK;
		scrollBalkenFarbe = Color.DARK_GRAY;
		historyTextFarbe = Color.BLACK;
		editorTextFarbe = Color.BLACK;
		textCursorFarbe = Color.BLACK;
		
		boolean debug = false;
		// zum testen folgende zeile einkommentieren, sonst auskommentieren
//		debug = true;
		if (debug) {
			int count = 10;
			for (int i = 0; i < count; i++) {
				HistoryEintrag zeile = new HistoryEintrag("["+getUhrzeit()+"] Name: <b>Dies</b> <i>ist</i> <u>Dummynachricht</u> Nummer "
						+ (i + 1) + " von " + count + ".");
				nachrichten.add(zeile);
			}
		}

		// ---

	}

	/**
	 * Nimmt die Font-Voreinstellungen vor. Instanziiert also die Felder:
	 * <blockquote> {@link #plainFont}, {@link #boldFont}, {@link #italicFont},
	 * {@link #editorFont} </blockquote> Kann auch dazu verwendet werden, die
	 * Schriftarten zur Laufzeit auf default zurueckzusetzen.
	 */
	private void fontVoreinstellungenVornehmen() {
		int fontSize = 17;
		int editorFontSize = 17;
		
		// hier kommen die gewuenschten FontFamilien
		// TODO Fonts aussuchen...
		String plainFontName = "Helvetica";
		String boldFontName = "Helvetica";
		String italicFontName = "Helvetica";
		String editorFontName = "Helvetica";
		String zweitFontName = Font.DIALOG; // falls ne Font nicht installiert
											// ist
		for (String fontName : GraphicsEnvironment
				.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()) {
			if (fontName.equals(plainFontName)) {
				plainFont = new Font(plainFontName, Font.PLAIN, fontSize);
			}
			if (fontName.equals(boldFontName)) {
				boldFont = new Font(boldFontName, Font.BOLD, fontSize);
			}
			if (fontName.equals(italicFontName)) {
				italicFont = new Font(italicFontName, Font.ITALIC, fontSize);
			}
			if (fontName.equals(editorFontName)) {
				editorFont = new Font(editorFontName, Font.PLAIN,
						editorFontSize);
			}
		}

		// falls eine der gewuenschten Fonts nicht installiert ist -> Dialog
		// sollte wohl ueberall installiert sein
		if (plainFont == null) {
			plainFont = new Font(zweitFontName, Font.PLAIN, fontSize);
		}
		if (boldFont == null) {
			boldFont = new Font(zweitFontName, Font.BOLD, fontSize);
		}
		if (italicFont == null) {
			italicFont = new Font(zweitFontName, Font.ITALIC, fontSize);
		}
		if (editorFont == null) {
			editorFont = new Font(zweitFontName, Font.PLAIN, editorFontSize);
		}
	}

	/**
	 * Zeichnet den Chat auf das angegebene <code>Graphics</code>-Object mit den
	 * angegebenen Koordinaten und Ma&szlig;en
	 * 
	 * @param g
	 *            das Graphics-Objekt, auf das der Chat gezeichnet werden soll
	 * @param x
	 *            die x-Koordinate der linken oberen Ecke des Chats
	 * @param y
	 *            die y-Koordinate der linken oberen Ecke des Chats
	 * @param b
	 *            die Breite des Chats
	 * @param h
	 *            die Hoehe des Chats
	 */
	public void zeichneChat(Graphics g, int x, int y, int b, int h) {
		// Zeitmessung true/false
		boolean bench = false;
		long start = 0;
		if (bench) {
			start = System.nanoTime();
		}

		if (repaint) {
			repaint = false;
			updateCache(b, h);
		}

		synchronized (cache) {
			// TODO image-observer: den Chatcontainer referenzieren?
			g.drawImage(cache, x, y, b, h, null);
			g.translate(x, y);
			//TODO wirklich nur alle 550ms den cursor repainten
			repaintCursor(g);
			g.translate(-x, -y);
		}

		if (bench) {
			double total = (System.nanoTime() - start) / 1000000000d;
			System.out.println("zeichneChat() dauerte: " + total + "ms");
		}
	}

	/**
	 * Zeichnet, wenn n&ouml;tig den TextCursor auf das angegebene
	 * Graphics-Objekt
	 * 
	 * @param g
	 *            das Graphics-Objekt auf das gezeichnet werden soll
	 * @see #cursor
	 */
	public void repaintCursor(Graphics g) {
		if (zeichneCursor) {
			Graphics2D g2D = (Graphics2D) g;
			g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
			g2D.setPaint(textCursorFarbe);
			g2D.draw(cursor);
		}
		zeichneCursor = !zeichneCursor;
	}

	/**
	 * Zeichnet den Cache neu. Dieser ist ein BufferedImage, welches in
	 * {@link #zeichneChat(Graphics, int, int, int, int) zeichneChat} zum
	 * zeichnen dieses Chats genutzt wird.
	 * 
	 * @param b
	 *            Breite des Chatbereichs
	 * @param h
	 *            Hoehe des Chatbereichs
	 */
	private void updateCache(int b, int h) {
		int x = 0, y = 0;
		Rectangle chatBereich = new Rectangle(x, y, b, h);

		BufferedImage cache = new BufferedImage(b, h, BufferedImage.TYPE_4BYTE_ABGR);
		Graphics2D g2D = cache.createGraphics();

		g2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);

		g2D.setFont(plainFont);
		FontMetrics fm = g2D.getFontMetrics();

		/*
		 * history-anzeige
		 */
		int arc = 10; // rundheitsmass der ecken des rechtecks
		g2D.setColor(hintergrundFarbe);
		g2D.fillRoundRect(x, y, b - 1, h - 25 - fm.getHeight(), arc, arc);
		g2D.setColor(rahmenFarbe);
		g2D.drawRoundRect(x, y, b - 1, h - 25 - fm.getHeight(), arc, arc);

		zeilenAnzahl = (h - 30 - 25) / fm.getHeight();
		// System.out.println("zeilenAnzahl: "+zeilenAnzahl+" startIndex: "+startIndex);

		// falls startIndex verringert werden kann weil genuegend zeilen uebrig
		if (startIndex + zeilenAnzahl >= alleZeilen && alleZeilen >= zeilenAnzahl){
			startIndex = alleZeilen - zeilenAnzahl;
			scrollBalkenIndex = alleZeilen - zeilenAnzahl;
		}
		
		
		g2D.setPaint(historyTextFarbe);
		y += 5;
		alleZeilen = 0;
		for (int index = 0; index < nachrichten.size(); index++) {
			HistoryEintrag zeile = nachrichten.get(index);
			int l = zeile.getWidth(g2D);
			// Da zwecks Zeilenumbruch selten eine Zeile voll ausgefuellt wird
			// nochmal ein Toleranz-Abzug
			// TODO Experimentieren noetig!
			int toleranz = 5;
			if (l <= (b - 1 - toleranz)) {
				if(alleZeilen >= startIndex && alleZeilen < startIndex+zeilenAnzahl)
					zeile.drawZeile(g2D, x + 5, y += fm.getHeight());
			} else {
				HistoryEintrag[] zeilen = zeile.split(b - 1 - toleranz, g2D);
				for (int j = 0; j < zeilen.length; j++) {
					if(alleZeilen >= startIndex && alleZeilen < startIndex+zeilenAnzahl)
						zeilen[j].drawZeile(g2D, x + 5, y += fm.getHeight());
					alleZeilen++;
				}
				alleZeilen--;
			}
			alleZeilen++;
		}
		y = h - 5 - fm.getHeight();
		
		
		/*
		 * Scrollbalken
		 */
		if(alleZeilen > zeilenAnzahl) {
			Rectangle hb = new Rectangle(x, y, b - 1, h - 25 - fm.getHeight());
//			scrollEinheiten = new Double(1.5 + (1.0d*alleZeilen)/zeilenAnzahl).intValue();
			scrollEinheiten = alleZeilen - zeilenAnzahl + 1;
			Double balkenGroesse = (1.0d * hb.height / scrollEinheiten);
			
//			System.out.println(balkenGroesse);
			
			int balkenBreite = 2;
//			System.out.println(scrollEinheiten + " "+scrollBalkenIndex);
			int sbX = hb.x+hb.width - balkenBreite - 3;
			double sbY = balkenGroesse * scrollBalkenIndex;
			g2D.setColor(scrollBalkenFarbe);
			g2D.fill(new Rectangle2D.Double(sbX, sbY, balkenBreite, balkenGroesse));
		}
		/*
		 * 'text-editor'
		 */
		g2D.setColor(hintergrundFarbe);
		Rectangle r = getEditorBounds(chatBereich);
		g2D.fillRoundRect(r.x, r.y, r.width - 1, r.height - 1, arc, arc);
		g2D.setColor(rahmenFarbe);
		g2D.drawRoundRect(r.x, r.y, r.width - 1, r.height - 1, arc, arc);

		y += fm.getHeight();

		String s = "";
		for (Character c : textZeile) {
			s += c;
		}
		// TODO simon: umlagern auf listener
		/*
		 * int zeilenBreite = b - 10; int diff = 0; if(fm.stringWidth(s) >
		 * zeilenBreite){ diff = fm.stringWidth(s) - zeilenBreite; }
		 * textZeilenIndex = 0; // System.out.print(diff);
		 * 
		 * while(diff > 0){ textZeilenIndex++; diff -=
		 * fm.charWidth(textZeile.get(textZeile.size()-1)); }
		 */
		// System.out.println("  "+textZeilenIndex);

		while (cursorIndex < textZeilenIndex) {
			textZeilenIndex--;
		}

		// TODO simon: umlagern auf listener
		// s = s.substring(textZeilenIndex);

		g2D.setFont(editorFont);
		g2D.setPaint(editorTextFarbe);
		g2D.drawString(s, r.x + 5, r.y + r.height - 5);
		
		//TODO simon: infos wieder entfernen spaeter
//		String infos = "startIndex: "+startIndex+" zeilenAnzahl: "+zeilenAnzahl+" alleZeilen: "+alleZeilen;
//		g2D.drawString(infos, r.x + 5, r.y + r.height + 15);
		
		// Cursor-Position berechnen
		FontMetrics edFm = g2D.getFontMetrics(editorFont);
		int i = 0;
		for (Character c : textZeile) {
			if (i == cursorIndex)
				break;
			x += edFm.charWidth(c);
			i++;

		}
		// Cursor-Position neu setzen
		cursor.setLine(x + 5, r.y + 2, x + 5, r.y + r.height - 3);
//		g2D.draw(cursor);

		// cache setzen
		if (this.cache == null) {
			this.cache = cache;
		} else {
			synchronized (this.cache) {
				this.cache = cache;
			}
		}

	}

	/**
	 * Gibt Position und Groesse des Chat-History-Bereichs als Rectangle an.
	 * 
	 * @param chatBereich Position und Groesse des ChatBereichs
	 * 
	 * @return Position und Groesse des Chat-History-Bereichs als Rectangle
	 *         relativ zum beinhaltenden ChatContainer
	 */
	public Rectangle getChatAnzeigeBounds(Rectangle chatBereich) {
		Rectangle editor = getEditorBounds(chatBereich);
		return new Rectangle(editor.x, chatBereich.y, editor.width, chatBereich.height - editor.height - 10);
	}

	/**
	 * Gibt Position und Groesse des Text-Editor-Bereichs als Rectangle an.
	 * 
	 * @param chatBereich Position und Groesse des ChatBereichs
	 * 
	 * @return Position und Groesse des Text-Editor-Bereichs als Rectangle
	 *         relativ zum ChatBereich
	 */
	public Rectangle getEditorBounds(Rectangle chatBereich) {
		// workaround, um an ein FontMetrics-Objekt zu kommen
		int fontheight = new BufferedImage(1, 1, BufferedImage.TYPE_4BYTE_ABGR)
				.createGraphics().getFontMetrics(editorFont).getHeight();
		int x = chatBereich.x;
		// TODO feinabstimmung
		int y = chatBereich.y + chatBereich.height - (fontheight + 15);
		int width = chatBereich.width;
		int height = fontheight;
		return new Rectangle(x, y, width, height);
	}

	

	/**
	 * Gibt den Chatverlauf als HistoryZeile-<code>List</code> zurueck.
	 * 
	 * @return den Chatverlauf als HistoryZeile-<code>List</code>
	 */
	public List<HistoryEintrag> getNachrichten() {
		return nachrichten;
	}

	public List<Character> getTextZeile() {
		return textZeile;
	}

	public int getStartIndex() {
		return startIndex;
	}

	public int getZeilenAnzahl() {
		return zeilenAnzahl;
	}

	public void setStartIndex(int startIndex) {
		this.startIndex = startIndex;
	}

	public void setZeilenAnzahl(int zeilenAnzahl) {
		this.zeilenAnzahl = zeilenAnzahl;
	}

	public void setTextZeilenIndex(int textZeilenIndex) {
		this.textZeilenIndex = textZeilenIndex;
	}

	public int getCursorIndex() {
		return cursorIndex;
	}

	/**
	 * Addiert die angegebene Zahl zum cursorIndex dazu
	 * 
	 * @param cursorIndex
	 *            der zu addierende Wert
	 */
	public void setCursorIndex(int cursorIndex) {
		this.cursorIndex += cursorIndex;
	}

	public int getScrollBalkenIndex() {
		return scrollBalkenIndex;
	}
	/**
	 * Addiert die angegebene Zahl zum scrollBalkenIndex dazu
	 * @param scrollBalkenIndex der zu addierende Wert
	 */
	public void setScrollBalkenIndex(int scrollBalkenIndex) {
		if((this.scrollBalkenIndex + scrollBalkenIndex) >= 0 && (this.scrollBalkenIndex + scrollBalkenIndex) < scrollEinheiten)
			this.scrollBalkenIndex += scrollBalkenIndex;
	}

	public int getScrollEinheiten() {
		return scrollEinheiten;
	}

	public int getAlleZeilen() {
		return alleZeilen;
	}

	public void setAlleZeilen(int alleZeilen) {
		this.alleZeilen = alleZeilen;
	}

	/**
	 * Informiert dieses Chatmodel darueber, dass beim naechsten Zeichnen der
	 * Cache erneuert werden muss.
	 */
	public void setChanged() {
		repaint = true;
	}
	
	/**
	 * Gibt die Uhrzeit zurueck
	 * @return die aktuelle Uhrzeit im Format hh:mm
	 */
	public static String getUhrzeit() {
		SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
		String uhrzeit = sdf.format(new Date());
		return uhrzeit;
	}
	/**
	 * Fuegt dem Model einen Output hinzu
	 * @param o der hinzuzufuegende Output
	 */
	public void addOutput(Output o){
		outputs.add(o);
	}
	/**
	 * Sendet eine Nachricht an alle Outputs
	 * @param s die zu sendende Nachricht
	 */
	public void sendeAnOutputs(String s){
		for (int i = 0; i < outputs.size(); i++) {
			outputs.get(i).verarbeiteNachricht(s);
		}
	}
	

	/**
	 * Druckt alle lokal verfuegbaren Fonts auf der Konsole aus.
	 * 
	 * @param args
	 *            keine Argumente
	 */
	public static void main(String[] args) {
		for (String s : GraphicsEnvironment.getLocalGraphicsEnvironment()
				.getAvailableFontFamilyNames()) {
			System.out.println(s);
		}
	}

	@Override
	public void verarbeiteNachricht(String s) {
		nachrichten.add(new HistoryEintrag(s));
		
	}
}
